package com.study.demo.aop.demo2.operationsource;

import com.study.demo.aop.demo2.ElvinOperation;
import com.study.demo.aop.demo2.parser.ElvinAnnotationParser;
import com.study.demo.aop.demo2.parser.SpringElvinAnnotationParser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: elvin
 * @time: 2019-05-23 14:36
 * @desc:
 **/
public class AnnotationElvinOperationSource implements ElvinOperationSource {

    private static final Collection<ElvinOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();

    protected final Log logger = LogFactory.getLog(getClass());

    private final Map<Object, Collection<ElvinOperation>> attributeElvin = new ConcurrentHashMap<>(1024);

    private final boolean publicMethodsOnly;

    private final Set<ElvinAnnotationParser> annotationParsers;

    //region constructor
    public AnnotationElvinOperationSource() {
        this(true);
    }

    public AnnotationElvinOperationSource(boolean publicMethodsOnly) {
        this.publicMethodsOnly = publicMethodsOnly;
        this.annotationParsers = new LinkedHashSet<>(1);
        this.annotationParsers.add(new SpringElvinAnnotationParser());
    }

    public AnnotationElvinOperationSource(ElvinAnnotationParser annotationParser) {
        this.publicMethodsOnly = true;
        Assert.notNull(annotationParser, "ElvinAnnotationParser must not be null");
        this.annotationParsers = Collections.singleton(annotationParser);
    }

    public AnnotationElvinOperationSource(ElvinAnnotationParser... annotationParsers) {
        this.publicMethodsOnly = true;
        Assert.notEmpty(annotationParsers, "At least one ElvinAnnotationParser needs to be specified");
        Set<ElvinAnnotationParser> parsers = new LinkedHashSet<>(annotationParsers.length);
        Collections.addAll(parsers, annotationParsers);
        this.annotationParsers = parsers;
    }

    public AnnotationElvinOperationSource(Set<ElvinAnnotationParser> annotationParsers) {
        this.publicMethodsOnly = true;
        Assert.notEmpty(annotationParsers, "At least one ElvinAnnotationParser needs to be specified");
        this.annotationParsers = annotationParsers;
    }
    //endregion

    @Override
    @Nullable
    public Collection<ElvinOperation> getElvinOperation(Method method, @Nullable Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return null;
        }

        Object cacheKey = getElvinKey(method, targetClass);
        Collection<ElvinOperation> cached = this.attributeElvin.get(cacheKey);

        if (cached != null) {
            return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
        }
        else {
            Collection<ElvinOperation> cacheOps = computeElvinOperations(method, targetClass);
            if (cacheOps != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
                }
                this.attributeElvin.put(cacheKey, cacheOps);
            }
            else {
                this.attributeElvin.put(cacheKey, NULL_CACHING_ATTRIBUTE);
            }
            return cacheOps;
        }
    }

    /**
     * Determine a cache key for the given method and target class.
     * <p>Must not produce same key for overloaded methods.
     * Must produce same key for different instances of the same method.
     * @param method the method (never {@code null})
     * @param targetClass the target class (may be {@code null})
     * @return the cache key (never {@code null})
     */
    protected Object getElvinKey(Method method, @Nullable Class<?> targetClass) {
        return new MethodClassKey(method, targetClass);
    }

    @Nullable
    private Collection<ElvinOperation> computeElvinOperations(Method method, @Nullable Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }

        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

        // First try is the method in the target class.
        Collection<ElvinOperation> opDef = findElvinOperations(specificMethod);
        if (opDef != null) {
            return opDef;
        }

        // Second try is the caching operation on the target class.
        opDef = findElvinOperations(specificMethod.getDeclaringClass());
        if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
            return opDef;
        }

        if (specificMethod != method) {
            // Fallback is to look at the original method.
            opDef = findElvinOperations(method);
            if (opDef != null) {
                return opDef;
            }
            // Last fallback is the class of the original method.
            opDef = findElvinOperations(method.getDeclaringClass());
            if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
                return opDef;
            }
        }

        return null;
    }


    @Nullable
    protected Collection<ElvinOperation> findElvinOperations(final Class<?> clazz) {
        return determineElvinOperations(parser -> parser.parseElvinAnnotations(clazz));
    }

    @Nullable
    protected Collection<ElvinOperation> findElvinOperations(final Method method) {
        return determineElvinOperations(parser -> parser.parseElvinAnnotations(method));
    }

    /**
     * Determine the cache operation(s) for the given {@link AnnotationElvinOperationSource.ElvinOperationProvider}.
     * <p>This implementation delegates to configured
     * {@link ElvinAnnotationParser}s for parsing known annotations into
     * Spring's metadata attribute class.
     * <p>Can be overridden to support custom annotations that carry
     * caching metadata.
     * @param provider the cache operation provider to use
     * @return the configured caching operations, or {@code null} if none found
     */
    @Nullable
    protected Collection<ElvinOperation> determineElvinOperations(AnnotationElvinOperationSource.ElvinOperationProvider provider) {
        Collection<ElvinOperation> ops = null;
        for (ElvinAnnotationParser annotationParser : this.annotationParsers) {
            Collection<ElvinOperation> annOps = provider.getElvinOperations(annotationParser);
            if (annOps != null) {
                if (ops == null) {
                    ops = new ArrayList<>();
                }
                ops.addAll(annOps);
            }
        }
        return ops;
    }

    /**
     * By default, only public methods can be made cacheable.
     */
    protected boolean allowPublicMethodsOnly() {
        return this.publicMethodsOnly;
    }


    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof AnnotationElvinOperationSource)) {
            return false;
        }
        AnnotationElvinOperationSource otherCos = (AnnotationElvinOperationSource) other;
        return (this.annotationParsers.equals(otherCos.annotationParsers) &&
                this.publicMethodsOnly == otherCos.publicMethodsOnly);
    }

    @Override
    public int hashCode() {
        return this.annotationParsers.hashCode();
    }


    /**
     * Callback interface providing {@link ElvinOperation} instance(s) based on
     * a given {@link ElvinAnnotationParser}.
     */
    @FunctionalInterface
    protected interface ElvinOperationProvider {

        /**
         * Return the {@link ElvinOperation} instance(s) provided by the specified parser.
         * @param parser the parser to use
         * @return the cache operations, or {@code null} if none found
         */
        @Nullable
        Collection<ElvinOperation> getElvinOperations(ElvinAnnotationParser parser);
    }
}
